(function(global) {

	var testDiv = document.createElement('div'),
		elSupportsExt = !!testDiv.__proto__
		emptyFn = function(){},
		hasOwn = Object.prototype.hasOwnProperty;

	global.Element || (global.Element = {});
	global.Event || (global.Event = {});
	var Element = global.Element;
	var Event = global.Event;

	function objectType(o) {
		return Object.prototype.toString.call(o).match(/ (\w+)\]$/)[1] || 'Unknown';
	}

  var updateScript = ('text' in document.createElement("script"))
		?
		function updateScript(html) {
			this.text = html || '';
		}
		:
		function updateScript(html) {
			(this.firstChild || this.appendChild(document.createTextNode(''))).data = html || '';
		}
	;

	function extend(d, s) {
		for (var p in s) {
			if (hasOwn.call(s, p)) {
				d[p] = s[p];
			}
		}
	}

	function evalScripts(html) {
		html.replace(/<script[^>]*>([^\x00]*?)<\/script>/gi, function(match) {
			globalEval(match[1]);
		})
	}

	var uid = '_protolove' +(+new Date);
	var globalEval = (function() {
		try {
			(1, eval)(uid + '=1');
			if (global[uid] === 1) {
				return function globalEval(src) {
					(1, eval)(src);
				};
			}
			delete global[uid];
		}
		catch(e) {}
		return function globalEval(src) {
			if (global.execScript) {
				return global.execScript(src);
			}
			(function() {
				global.eval.call(global, src);
			})();
		}
		var globalEval = null;
	})();

	Element.extend = function extend(el) {
		if (!el) {
			return undefined;
		}
		if (typeof el == 'function') {
			return Element.extend(document).observe('DOMContentLoaded', el);
		}
		if (objectType(el) == 'String') {
			if (el.charAt(0) == '<') {
				return Element.build(el, arguments[1], arguments[2]);
			}
			el = document.getElementById(el);
		}
		if (
			(!elSupportsExt || HTMLElement.prototype.show != el.show)
			&& typeof el._extendedByProtoLove == 'undefined'
		) {
			for (var p in Element.Methods) {
				if (!hasOwn.call(Element.Methods, p)) {
					continue;
				}
				el[p] = Element.Methods[p];
			}
			el._extendedByProtoLove = emptyFn;
		}
		return el;
	}

	Element.build = function build(htmlOrTag, attr, children) {
		var wrap = document.createElement('div'), el, child, i = 0, p;
		if (htmlOrTag.match(/^[\w_-]+$/)) {
			htmlOrTag = '<' + htmlOrTag + ' />';
		}
		wrap.innerHTML = htmlOrTag;
		if (objectType(attr) == 'Array' || attr && attr.nodeType == 1) {
			children = attr;
			attr = null;
		}
		el = Element.extend(wrap.firstChild);
		if (attr) {
			for (p in attr) {
				if (hasOwn.call(attr, p)) {
					el.setAttribute(p, attr[p]);
				}
			}
		}
		if (children && objectType(children) == 'Array') {
			while ((child = children[i++])) {
				el.appendChild(child);
			}
		}
		else if (children) {
			el.appendChild(children);
		}
		wrap = null;
		return el;
	};

	function makeGeneric(name) {
		Element[name] = function(el) {
			var args = Array.prototype.slice.call(arguments),
				el = Element.extend(args.shift());
			return Element.Methods[name].apply(el, args);
		};
	}

	// add methods to element prototype
	Element.addMethods = function addMethods(methods) {
		for (var p in methods) {
			if (!hasOwn.call(methods, p)) {
				continue;
			}
			Element.Methods[p] = methods[p];
			if (elSupportsExt) {
				HTMLElement.prototype[p] = methods[p];
			}
			makeGeneric(p);
		}
		return Element;
	};

	// Element prototype methods
	var anonElCounter = 1;
	Element.Methods = {
		identify: function identify() {
			var id;
			if (this.id && typeof this.id !== 'string') {
				// node has a property id such as a <form> tag with an input with name "id"
				// if the node also has an object named cloneNode, there will be a fatal error
				var clone = this.cloneNode(false);
				id = clone.id;
				clone = null;
			}
			else {
				id = this.id;
			}
			if (!id || typeof id !== 'string') {
				id = 'anonymous_element_' + (anonElCounter++);
				if (typeof this.setAttribute == 'function') {
					this.setAttribute('id', id);

				} else {
					this.id = id;
				}
			}
			return id;
		},
		observe: function observe(type, handler) {
			if (type == 'dom:loaded') {
				type = 'DOMContentLoaded';
			}
			this.addEventListener(type, handler, false);
			return this;
		},
		stopObserving: function stopObserving(type, handler) {
			if (type == 'dom:loaded') {
				type = 'DOMContentLoaded';
			}
			this.removeEventListener(type, handler, false);
			return this;
		},
		show: function show() {
			this.style.display = '';
			return this;
		},
		hide: function hide() {
			this.style.display = 'none';
			return this;
		},
		toggle: function toggle() {
			return this.style.display = (this.style.display == 'none' ? '' : 'none');
		},
		visible: function visible() {
			return this.style.display != 'none';
		},
		getOpacity: function getOpacity(o) {
			return parseFloat(this.style.opacity) || 1.0;
		},
		setOpacity: function setOpacity(o) {
			this.style.opacity = (o == 1 || o === '') ? '' : (o < 0.00001) ? 0 : o;
			return this;
		},
		/**
		 * Set the css style of a node. Attribute names can be hyphenated (e.g. "z-index")
		 *   or camelized (e.g. "zIndex"). Optionally an Object with attribute-value pairs may be passed.
		 * @param {String} attr  The attribute name
		 * @param {String} value  The value to set
		 * @return {HTMLElement}
		 * @chainable
		 * @example
		 *   $('fruit').setStyle('z-index', '1');
		 *   $('fruit').setStyle('backgroundColor', 'red');
		 *   $('fruit').setStyle({
		 *     textAlign: 'white',
		 *     'font-weight': 'bold'
		 *   });
		 *   $('vegetable').setStyle('opacity:0.5; padding-top: 5px');
		 */
		setStyle: function setStyle(attr, value) {
			if (arguments.length == 2 && objectType(attr) == 'String') {
				if (attr == 'opacity') {
					return this.setOpacity(value);
				}
				if (attr.indexOf('-') > -1) {
					attr = camelize(attr);
				}
				this.style[attr] = value;
			}
			else {
				if (typeof attr == 'string') {
					attr = styleStringToObject(attr);
				}
				for (var name in attr) {
					if (!hasOwn.call(attr, name)) {
						continue;
					}
					this.setStyle(name, attr[name]);
				}
			}
			return this;
		},
		/**
		 * Set the css style of a node. Attribute names can be hyphenated (e.g. "z-index")
		 *   or camelized (e.g. "zIndex"). Optionally an Object with attribute-value pairs may be passed.
		 * @param {String} attr  The attribute name
		 * @return {String}
		 */
		getStyle: function getStyle(attr) {
			if (attr == 'opacity') {
				return this.getOpacity();
			}
			if (attr.indexOf('-') > -1) {
				attr = camelize(attr);
			}
			return this.style[attr];
		},
		/**
		 * Return true if the node has the given css class name
		 *
		 * @param {String} className
		 * @return {Boolean}
		 */
		hasClassName: function hasClassName(c) {
			if (this.classList && this.classList.contains) {
				return this.classList.contains(c);
			}
			return (' ' + this.className + ' ').indexOf(' ' + c + ' ') > -1;
		},
		/**
		 * Add a css class name to the node
		 *
		 * @param {String} className
		 * @return {HTMLElement}
		 * @chainable
		 */
		addClassName: function addClassName(c) {
			if (this.classList && this.classList.add) {
				this.classList.add(c);
			}
			else if (!this.hasClassName(c)) {
				this.className = (this.className.length == 0 ? c : this.className + ' ' + c);
			}
			return this;
		},
		/**
		 * Remove a css class name from the node
		 *
		 * @param {String} className
		 * @return {this}
		 * @chainable
		 */
		removeClassName: function removeClassName(c) {
			if (this.classList && this.classList.remove) {
				this.classList.remove(c);
			}
			else {
				this.className = (' ' + this.className + ' ').replace(' ' + c + ' ', ' ');
			}
			return this;
		},
		toggleClassName: function toggleClassName(c) {
			if (this.classList && this.classList.toggle) {
				this.classList.toggle(c);
			}
			else {
				this[this.hasClassName(c) ? 'removeClassName' : 'addClassName'](c);
			}
			return this;
		},
		match: function match(basicCss) {
			basicCss = basicCss.toLowerCase();
			var anc = basicCss.split(/\s+/);
			if (!elMatchesCssPart(this, anc.pop())) {
				return false;
			}
			var cssPart, doesMatch = true, el = this;
			while ((cssPart = anc.pop()) && doesMatch) {
				doesMatch = false;
				do {
					el = el.parentNode;
					if (elMatchesCssPart(el, cssPart)) {
						doesMatch = true;
						break;
					}
				} while (el != document);
				if (el == document) {
					return false;
				}
			}
			return doesMatch;
		},
		up: function up(basicCssOrNumber) {
			basicCssOrNumber || (basicCssOrNumber = 1);
			var el = this;
			if (objectType(basicCssOrNumber) == 'Number') {
				while (basicCssOrNumber-- && el) {
					el = el.parentNode;
				}
				return el;
			}
			return this.match(basicCssOrNumber) || undefined;
		},
		down: function down(basicCssOrNumber) {
			basicCssOrNumber || (basicCssOrNumber = 1);
			var el = this;
			if (objectType(basicCssOrNumber) == 'Number') {
				while (basicCssOrNumber-- && el) {
					el = el.firstChild;
				}
				return el;
			}
			var all = this.getElementsByTagName('*'), i = 0;
			while ((el = all[i++])) {
				if (elMatchesCssPart(el, basicCssOrNumber)) {
					return el;
				}
			}
			return undefined;
		},
		delegate: function delegate(type, selector, handler) {
			this.observe(type, function(event) {
				var target = event.target;
				do {
					if ('match' in Element.extend(target) && target.match(selector)) {
						handler.call(target, event);
						return;
					}
				} while ((target = target.parentNode) && target != document);
			});
			return this;
		},
		// stopDelegating ?
		update: function update(html) {
			if (html && html.nodeType == 1) {
				this.innerHTML = '';
				this.appendChild(html);
			}
			else {
				if (this.tagName.toLowerCase() == 'script') {
					updateScript.call(this, html);
				}
				else {
					this.innerHTML = html;
					evalScripts(html);
				}
			}
			return this;
		},
		on: function on(a, b, c) {
			if (typeof b == 'function') {
				return this.observe(a, b);
			}
			return this.delegate(a, b, c);
		}
		// un ?
	};

	// helper functions
	function camelize(str) {
		var parts = str.split('-'), result = parts[0], i, len;
		parts = Array.prototype.slice.call(parts, 1);
		for (i = 0, len = parts.length; i < len; i++) {
			result += parts[i].substring(0,1).toUpperCase() + parts[i].substring(1);
		}
		return result;
	}
	function styleStringToObject(styles) {
		var styleList = styles.split(';'), match, obj = {}, i, len;
		for (i = 0, len = styleList.length; i < len; i++) {
			if ((match = styleList[i].match(/^\s*([a-z0-9-]+)\s*:\s*(.+)\s*$/))) {
				obj[match[1]] = match[2];
			}
		}
		return obj;
	}
	function elMatchesCssPart(el, cssPart) {
		var def = cssPart.match(/^([\w_-]*)(?:#([\w_-]+))?(?:\.([\w_-]+))?$/);
		if (!def) {
			throw new Error('ProtoLove - unrecognized CSS selector part `' + cssPart + '`');
		}
		return !(
			(!!def[1] && el.tagName.toLowerCase() != def[1]) ||
			(!!def[2] && el.id != def[2]) ||
			(!!def[3] && (
				(el.classList && el.classList.contains && !el.classList.contains(def[3])) ||
				((' ' + el.className + ' ').indexOf(' ' + def[3] + ' ') == -1)
			))
		);
	}
	//
	// SHIMS for older browsers (not tuned for performance)
	//

	// Unable to set table innerHTML (and we'll assume select innerHTML) (IE<=8)
	if ((function() {
		try {
			var table = document.createElement("table");
			table.innerHTML = "<tbody><tr><td>test<\/td><\/tr><\/tbody>";
			return false;
		}
		catch (e) {
			// error setting table innerHTML
			return true;
		}
	})()) {
	 var shells = {
			'sele' : ['<select multiple="multiple">', '<\/select>', 1],
			'tr'   : ['<table><tbody>', '<\/tbody>\<\/table>', 2],
			'td'   : ['<table><tbody><tr>', '<\/tr><\/tbody><\/table>', 3],
			'thea' : ['<table>', '<\/table>', 1],
			'col'  : ['<table><tbody><\/tbody><colgroup>', '<\/colgroup><\/table>', 2]
		};
		shells.tbod = shells.tfoo = shells.colg = shells.capt = shells.thea;

		Element.Methods.update = function update(html) {
			html = html || '';

			if (html.nodeType == 1) {
				this.innerHTML = '';
				this.appendChild(html);
			}
			else {
				// based on Prototype 1.6.1rc2 and jQuery 1.3.2
				var tag = this.tagName.toLowerCase().slice(0,4);

				if (tag == 'scri') {
					updateScript.call(this, html);
				}
				else {
					var wrap = shells[tag];
					var div = document.createElement('div');
					div.innerHTML = wrap[0] . html . wrap[1];
					var level = wrap[2];

					while (level--) {
						div = div.lastChild;
					}
					this.replaceNode(div);
					// TODO: copy attributes and events from wrapped items
				}
				evalScripts(html);
			}
			return this;
		};
	}
	else {
		shells = null;
	}
	// Support for getElementsByClassName
	if (!document.getElementsByClassName) {
		Element.Methods.getElementsByClassName = function getElementsByClassName(c) {
			var els = this.getElementsByTagName('*'), results = [], i, len = els.length, cnt = 0;
			for (i = 0; i < len; i++) {
				if ((' ' + els[i].className + ' ').indexOf(' ' + c + ' ') > -1) {
					results[cnt++] = els[i];
				}
			}
			return results;
		};
	}
	// Supports alpha filter instead of opacity (IE<=8)
	testDiv.innerHTML = '<span style="opacity:0.42"></span>';
	if (!testDiv.firstChild.style.opacity) {
		Element.Methods.getOpacity = function getOpacity() {
			var match = this.style.filter.match(/alpha\(opacity=(.*)\)/);
			if (match[1]) {
				return parseFloat(match[1]) / 100;
			}
			return 1.0;
		};
		Element.Methods.setOpacity = function setOpacity(o) {
			this.style.filter = this.style.filter.replace(/alpha\([^\)]*\)/gi,'') + 'alpha(opacity=' + (o * 100) + ')';
			return this;
		};
	}
	// No support for hasAttribute (IE<=8)
	if (!testDiv.hasAttribute) {
		Element.Methods.hasAttribute = function hasAttribute(name) {
			var node = this.getAttributeNode(name);
			return (node && node.specified);
		};
	}
	// Fix IE attachEvent madness and DOMContentLoaded support (IE<=8)
	if (!document.addEventListener && document.attachEvent) {
		var observers = {}, domLoadedHandlers = [], elUid = 0;
		function getElUid(el) {
			var existing = el.getAttribute('elUid');
			if (existing) {
				return existing;
			}
			el.setAttribute('elUid', ++elUid);
			return elUid;
		}
		Element.Methods.observe = function observe(type, handler) {
			if (type == 'dom:loaded' || type == 'DOMContentLoaded') {
				if (domLoadedHandlers.length == 0) {
					pollDOMLoad();
				}
				domLoadedHandlers.push(handler);
			}
			else {
				var el = this, idx = getElUid(this) + type, fixed;
				if (!observers[idx]) {
					observers[idx] = [];
					fixed = function() {
						// call handler in Element scope and add missing event properties
						var evt = window.event, fn, i = 0;
						try {
							// these properties are by spec read only, so we may get an error
							evt.target = evt.srcElement || el;
							evt.currentTarget = el;
							evt.type = type;
						}
						catch (e) {}
						while ((fn = observers[idx][i++])) {
							fn.call(el, evt);
						}
					};
					observers[idx]._fixed = fixed;
					this.attachEvent('on' + type, fixed);
				}
				observers[idx].push(handler);
			}
			return this;
		};
		Element.Methods.stopObserving = function stopObserving(type, handler) {
			if (type == 'dom:loaded' || type == 'DOMContentLoaded') {
				removeDOMContentLoadedListener(handler);
				return this;
			}
			var idx = getElUid(this) + type, handlers = observers[idx], fn, i = 0, filtered = [];
			if (handlers) {
				while ((fn = handlers[i++])) {
					if (fn != handler) {
						filtered.push(fn);
					}
				}
				if (filtered.length == 0) {
					this.detachEvent('on' + type, handlers._fixed);
					delete observers[idx];
				}
				else {
					filtered._fixed = handlers._fixed;
					observers[idx] = filtered;
				}
			}
			return this;
		};
		// doScroll support for DOMContentLoaded by Diego Perini
		function pollDOMLoad() {
			try {
				document.documentElement.doScroll('left');
			}
			catch (e) {
				setTimeout(pollDOMLoad, 10);
				return;
			}
			for (var i = 0, len = domLoadedHandlers.length; i < len; i++) {
				domLoadedHandlers[i].call(document, window.event);
			}
		}
	}
	//
	// END SHIMS for non-compliant browsers
	//

	Element.addMethods(Element.Methods);

	document.ready = function ready(fn) {
		Element.extend(document).observe('DOMContentLoaded', fn);
	};

	var prop, i = 0, props = 'type target currentTarget eventPhase bubbles cancelable detail screenX screenY clientX clientY metaKey ctrlKey altKey shiftKey metaKey button'.split(' ');
	while ((prop = props[i++])) {
		Event['get' + prop.charAt(0).toUpperCase() + prop.slice(1)] = (function() {
			return function (evt) {
				return evt[prop];
			};
		})(prop);
	}
	extend(Event, {
		getRelatedTarget: function getRelatedTarget(evt) {
			return evt.relatedTarget || evt[(evt.target == evt.fromElement ? 'to' : 'from') + 'Element'];
		},
		getPointerX: function getPointerX(evt) {
			if (typeof evt.pageX == 'number') {
				return evt.pageX;
			}
			var doc = document.documentElement, body = document.body;
			return evt.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0) - (doc && doc.clientLeft || body && body.clientLeft || 0);
		},
		getPointerY: function getPointerY(evt) {
			if (typeof evt.pageX == 'number') {
				return evt.pageX;
			}
			var doc = document.documentElement, body = document.body;
			return evt.clientY + (doc && doc.scrollTop  || body && body.scrollTop  || 0) - (doc && doc.clientTop  || body && body.clientTop  || 0);
		},
		getWhich: function getWhich(evt) {
			if (!evt.which && (typeof evt.charCode == 'number' || evt.keyCode)) {
				return evt.charCode || evt.keyCode;
			}
			return evt.which;
		},
		stopPropagation: function stopPropagation(evt) {
			if (evt.stopPropagation) {
				evt.stopPropagation();
			}
			else {
				evt.cancelBubble = false;
			}
			return Event;
		},
		preventDefault: function preventDefault(evt) {
			if (evt.preventDefault) {
				evt.preventDefault();
			}
			else {
				evt.returnValue = false;
			}
			return Event;
		},
		stop: function stop(evt) {
			Event.preventDefault(evt);
			Event.stopPropagation(evt);
			return Event;
		},
		isLeftClick: function isLeftClick(evt) {
			return (typeof evt.which == 'number' && evt.which == 3) || evt.button === 2;
		},
		isMiddleClick: function isMiddleClick(evt) {
			return (typeof evt.which == 'number' && evt.which == 2) || evt.button === 4;
		},
		isRightClick: function isRightClick(evt) {
			return (typeof evt.which == 'number' && evt.which == 1) || evt.button === 1;
		},
		up: function up(evt, basicCssOrNumber) {
			return Element.extend(Event.getTarget(evt)).up(basicCssOrNumber);
		},
		KEY_BACKSPACE: 8,
		KEY_DELETE:    46,
		KEY_DOWN:      40,
		KEY_END:       35,
		KEY_ESC:       27,
		KEY_HOME:      36,
		KEY_INSERT:    45,
		KEY_LEFT:      37,
		KEY_PAGEDOWN:  34,
		KEY_PAGEUP:    33,
		KEY_RETURN:    13,
		KEY_RIGHT:     39,
		KEY_TAB:       9,
		KEY_UP:        38
	});
	Event.getPageX = Event.getPointerX;
	Event.getPageY = Event.getPointerY;
	Event.element = Event.getTarget;

	var extend = null,
		build = null,
		addMethods = null,
		observe = null,
		stopObserving = null,
		show = null,
		hide = null,
		getOpacity = null,
		setOpacity = null,
		setStyle = null,
		getStyle = null,
		hasClassName = null,
		addClassName = null,
		removeClassName = null,
		match = null,
		delegate = null,
		on = null,
		update = null,
		hasAttribute = null,
		getElementsByClassName = null,
		ready = null,
		getTarget = null,
		getRelatedTarget = null,
		getPointerX = null,
		getPointerY = null,
		getWhich = null,
		stopPropagation = null,
		preventDefault = null,
		stop = null;

	// create ProtoLove if not exists
	global.ProtoLove = global.ProtoLove || {};
	// export Element, Event
	global.Element = global.Element || Element;
	global.Event = global.Event || Event;
	// export $
	global.ProtoLove.$ = Element.extend;
	global.$ = global.$ || Element.extend;

})(this);
